﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace TfsRollUp.Core
{
    /// <summary>
    /// Manages the workitemstore and other objects that needs to be cached
    /// </summary>
    public class Service : ITeamFoundationService, IDisposable
    {
        private ThreadLocal<Dictionary<Uri, Store>> _store = new ThreadLocal<Dictionary<Uri, Store>>(() => new Dictionary<Uri, Store>());
        private TeamFoundationRequestContext _requestContext;
        private Receiver _receiver;
        private bool _disposed;

        /// <summary>
        /// Global settings to this service
        /// </summary>
        public Settings Settings { get; private set; }

        /// <summary>
        /// The tfs system account sid, used to guard against circular updates
        /// </summary>
        public string SystemAccountSid { get; private set; }

        /// <summary>
        /// Queue with workitem roots that is to be processed
        /// </summary>
        public BlockingCollection<WorkItem> WorkItemRootQueue { get; private set; }

        /// <summary>
        /// Take all processing requests and have them convoyed into the WorkItemRootQueue
        /// Disallow processing of the same root workitem within Constants.WorkItemTimeToLiveMilliSeconds
        /// </summary>
        private void PreProcessWorkItem()
        {
            TeamFoundationTrace.MethodEntry();
            var blockList = new Dictionary<string, DateTime>();

            // Block while waiting for new workitems put in the queue from ProcessWorkItem
            foreach (var workItem in WorkItemRootQueue.GetConsumingEnumerable())
            {
                var workItemKey = workItem.CollectionTeamProjectIdKey();
                if (blockList.ContainsKey(workItemKey))
                {
                    var originalTime = blockList[workItemKey];
                    var delta = DateTime.UtcNow - originalTime;
                    if (delta.TotalMilliseconds > Constants.WorkItemTimeToLiveMilliSeconds)
                    {
                        // Reset timer
                        blockList[workItemKey] = DateTime.UtcNow;
                        Task.Factory.StartNew(() => ProcessWorkItem(workItem));
                    }
                    else // Block the workitem, i.e. throw it away
                    {
                        TeamFoundationTrace.Info("{0}.PreProcessWorkItem: WorkItem {1} showed up within {2} (ms) of the block-time and will be thrown out", Constants.TfsRollUpPlugin, workItem.Id, delta.TotalMilliseconds);
                    }
                }
                else
                {
                    blockList.Add(workItemKey, DateTime.UtcNow);
                    Task.Factory.StartNew(() => ProcessWorkItem(workItem));
                }
                BlockListHouseCleaning(blockList);
            }
            TeamFoundationTrace.MethodExit();
        }

        private void ProcessWorkItem(WorkItem workItem)
        {
            TeamFoundationTrace.MethodEntry();
            TeamFoundationTrace.Info("{0}.ProcessWorkItem: Finally, at last, something to do: processing WorkItem:{1}", Constants.TfsRollUpPlugin, workItem.Id);

            var fieldCollection = FieldCollection(workItem.Store.TeamProjectCollection.Uri);
            if (fieldCollection == null)
            {
                TeamFoundationTrace.Error("{0}.ProcessWorkItem: unexpected null for fieldCollection for uri:{1}", Constants.TfsRollUpPlugin, workItem.Store.TeamProjectCollection.Uri);
                TeamFoundationTrace.MethodExit();
                return;
            }

            var fieldList = fieldCollection.GetRollUpFieldList(workItem.Fields[CoreField.TeamProject].Value.ToString(), workItem.Type.Name);
            if (fieldList == null || fieldList.Length == 0)
            {
                TeamFoundationTrace.Verbose("{0}.ProcessWorkItem: RollUp fields for WorkItemId:{1} was not found in TfsCollection:'{2}' TeamProject:'{3}' WorkItemType:'{4}'", Constants.TfsRollUpPlugin, workItem.Id, workItem.Store.TeamProjectCollection.Name, (string)workItem.Fields[CoreField.TeamProject].Value, workItem.Type.Name);
                TeamFoundationTrace.MethodExit();
                return;
            }

            var tree = workItem.CalculateTree(fieldList, Settings);
            if (tree != null && tree.Count > 0)
            {
                var batchSaveErrors = workItem.SaveTree(tree);
                if (batchSaveErrors != null && batchSaveErrors.Length > 0)
                {
                    TeamFoundationTrace.Error("{0}.ProcessWorkItem: BatchSave failed. {1} errors occurred. Starting recalculation", Constants.TfsRollUpPlugin, batchSaveErrors.Length);
                    SendErrorsToTrace(batchSaveErrors);
                    tree = workItem.CalculateTree(fieldList, Settings);
                    batchSaveErrors = workItem.SaveTree(tree);
                    if (batchSaveErrors != null && batchSaveErrors.Length > 0)
                    {
                        TeamFoundationTrace.Error("{0}.ProcessWorkItem: Retry BatchSave failed. {1} errors occurred", Constants.TfsRollUpPlugin, batchSaveErrors.Length);
                        SendErrorsToTrace(batchSaveErrors);
                    }
                }
            }
            TeamFoundationTrace.MethodEntry();
        }

        private void SendErrorsToTrace(BatchSaveError[] batchSaveErrors)
        {
            foreach (var error in batchSaveErrors)
            {
                TeamFoundationTrace.Error("{0}.SendErrorsToTrace: WorkItem:{1} Exception:{2}", Constants.TfsRollUpPlugin, error.WorkItem.Id, error.Exception);
            }
        }

        private static void BlockListHouseCleaning(Dictionary<string, DateTime> blockList)
        {
            if (blockList.Count > 512)
            {
                var timeNow = DateTime.UtcNow;
                foreach (var key in blockList.Keys)
                {
                    if (timeNow.Subtract(blockList[key]).TotalMilliseconds > Constants.WorkItemTimeToLiveMilliSeconds)
                    {
                        blockList.Remove(key);
                    }
                }
            }
        }

        #region ITeamFoundationService
        /// <summary>
        /// Stop service
        /// </summary>
        /// <param name="systemRequestContext"></param>
        public void ServiceEnd(TeamFoundationRequestContext systemRequestContext)
        {
            TeamFoundationTrace.MethodEntry(systemRequestContext);
            _store = null;
            _receiver.ServiceOff();
            _receiver = null;
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Flush cached objects
        /// </summary>
        public void ServiceFlushCache()
        {
            TeamFoundationTrace.MethodEntry();
            _store = new ThreadLocal<Dictionary<Uri, Store>>(() => new Dictionary<Uri, Store>());
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Initialize the service
        /// </summary>
        /// <param name="systemRequestContext"></param>
        public void ServiceStart(TeamFoundationRequestContext systemRequestContext)
        {
            TeamFoundationTrace.MethodEntry(systemRequestContext);
            _requestContext = systemRequestContext;
            Settings = Settings.FromString(systemRequestContext);
            _receiver = new Receiver(this);
            if (_receiver.ServiceOn())
            {
                TeamFoundationTrace.Info("{0}.ServiceStart: The Receiver is turned on", Constants.TfsRollUpPlugin);
            }
            SystemAccountSid = _requestContext.UserContext.Identifier;

            // Use this collection to arrange requests into  a convoy
            WorkItemRootQueue = new BlockingCollection<WorkItem>();
            Task.Factory.StartNew(() => { PreProcessWorkItem(); });
            TeamFoundationTrace.MethodExit();
        }
        #endregion ITeamFoundationService

        /// <summary>
        /// The WorkItemStore, one store is saved per uri per thread <see cref="WorkItemRootQueue"/>
        /// </summary>
        public WorkItemStore WorkItemStore(Uri uri)
        {
            if (!_store.Value.ContainsKey(uri))
            {
                _store.Value.Add(uri, new Store(uri));
            }
            return _store.Value[uri].WorkItemStore;
        }

        /// <summary>
        /// The WorkItemStore, one store is saved per uri per thread <see cref="WorkItemRootQueue"/>
        /// </summary>
        public WorkItemStore WorkItemStore(TeamFoundationRequestContext requestContext)
        {
            var uri = Core.Store.GetUriFromRequestContext(requestContext);
            return WorkItemStore(uri);
        }

        /// <summary>
        /// Returns the workitem field collection
        /// </summary>
        /// <param name="uri">The key</param>
        /// <returns>The collection of Alm fields</returns>
        public WorkItemFieldCollection FieldCollection(Uri uri)
        {
            if (!_store.Value.ContainsKey(uri))
            {
                _store.Value.Add(uri, new Store(uri));
            }
            return _store.Value[uri].WorkItemFieldCollection;
        }

        /// <summary>
        /// Returns the workitem field collection
        /// </summary>
        /// <param name="requestContext"></param>
        /// <returns>The collection of Alm fields</returns>
        public WorkItemFieldCollection FieldCollection(TeamFoundationRequestContext requestContext)
        {
            var uri = Core.Store.GetUriFromRequestContext(requestContext);
            return FieldCollection(uri);
        }

        #region IDisposable

        /// <summary>
        /// Kill
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (_receiver != null)
                {
                    ((IDisposable)_receiver).Dispose();
                    _receiver = null;
                }
                if (_store != null)
                {
                    _store.Dispose();
                }

                // Set the sentinel.
                _disposed = true;

                // Suppress finalization of this disposed instance.
                if (disposing)
                {
                    GC.SuppressFinalize(this);
                }
            }
        }

        /// <summary>
        /// Prepare kill
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>
        /// PrePreKill
        /// </summary>
        ~Service()
        {
            Dispose(false);
        }
        #endregion
    }
}
